function downloadText(fileName, text) {
  const url = window.URL || window.webkitURL || window;
  const blob = new Blob([text]);
  const saveLink = document.createElement("a");
  saveLink.href = url.createObjectURL(blob);
  saveLink.download = fileName;
  saveLink.click();
}

// Cache for storing geo data to avoid duplicate requests
const geoCache = new Map();

// Improved getGeo function with caching and error handling
let getGeo = async function(id, type) {
  const cacheKey = `${id}.${type}`;
  
  // Return cached result if available
  if (geoCache.has(cacheKey)) {
    return geoCache.get(cacheKey);
  }

  try {
    const geojson = await new Promise((resolve, reject) => {
      DeviceQuery.queryDeviceByIds(
        {
          devIds: [
            {
              devId: id,
              devType: type,
              distribution: 0,
              provinceId: ""
            },
            {
              devId: id,
              devType: type,
              distribution: 1,
              provinceId: ""
            }
          ]
        },
        res => {
          if (res) {
            const result = res.toGeojson();
            geoCache.set(cacheKey, result); // Cache the result
            resolve(result);
          } else {
            geoCache.set(cacheKey, null); // Cache null to avoid retrying
            resolve(null);
          }
        },
        err => {
          geoCache.set(cacheKey, null); // Cache null to avoid retrying
          resolve(null);
        }
      );
    });
    return geojson;
  } catch (error) {
    geoCache.set(cacheKey, null); // Cache null to avoid retrying
    return null;
  }
};

// Process nodes in batches to avoid memory issues
async function processNodesInBatches(nodes, batchSize = 50) {
  const noFindNodeIds = [];
  const noFindPowerSourceIds = [];
  const noFindStationIds = [];
  
  for (let i = 0; i < nodes.length; i += batchSize) {
    console.log('i',i,'/',nodes.length)
    const batch = nodes.slice(i, i + batchSize);
    
    // Process all items in parallel for this batch
    await Promise.all(batch.map(async (item) => {
      // Process node geo
      const idParts = item.id.split(".");
      let geo = await getGeo(idParts[0], idParts[1]);
      
      if (geo) {
        const G = geo.features[0].geometry.type === "Point" 
          ? geo.features[0].geometry 
          : turf.center(geo.features[0].geometry).geometry;
        item.userData.geo = G;
        item.userData.name = geo.features[0].properties.devName;
      } else {
        noFindNodeIds.push(item.id);
      }

      // Process PowerSource if needed
      if (item.userData?.PowerSource === "True") {
        let stationGeo = await getGeo(item.userData.stationId, "zf01");
        if (stationGeo) {
          item.userData.name = stationGeo.features[0].properties.devName;
        } else {
          noFindPowerSourceIds.push(item.id);
        }
      }

      // Process station if needed
      if (idParts[1] === "0311" && item.userData?.stationPSRType && item.userData?.stationPSRID) {
        let stGeo = await getGeo(item.userData.stationPSRID, item.userData.stationPSRType);
        if (stGeo) {
          item.userData.stname = stGeo.features[0].properties.devName;
        } else {
          noFindStationIds.push(item.id);
        }
      }
    }));
  }

  return { noFindNodeIds, noFindPowerSourceIds, noFindStationIds };
}

// Process links in batches
async function processLinksInBatches(links, batchSize = 50) {
  const noFindLinkPsrIds = [];
  const nolineLinkST = [];
  
  for (let i = 0; i < links.length; i += batchSize) {
    console.log('i',i,'/',links.length)
    const batch = links.slice(i, i + batchSize);
    
    await Promise.all(batch.map(async (item) => {
      if (item.userData?.PSRType && item.userData?.PSRID) {
        // Has PSRType and PSRID - query coordinates
        let geo = await getGeo(item.userData.PSRID, item.userData.PSRType);
        if (geo) {
          item.userData.geo = geo.features[0].geometry;
          item.userData.name = geo.features[0].properties.devName;
        } else {
          noFindLinkPsrIds.push(`${item.source}-${item.target}`);
        }
      } else {
        // No PSRType/PSRID - query source and target coordinates
        const sourceParts = item.source.split(".");
        const targetParts = item.target.split(".");
        
        const [sourceGeo, targetGeo] = await Promise.all([
          getGeo(sourceParts[0], sourceParts[1]),
          getGeo(targetParts[0], targetParts[1])
        ]);
        
        if (sourceGeo && targetGeo) {
          const start = sourceGeo.features[0].geometry.type === "Point"
            ? sourceGeo.features[0].geometry.coordinates
            : turf.center(sourceGeo.features[0].geometry).geometry.coordinates;
            
          const end = targetGeo.features[0].geometry.type === "Point"
            ? targetGeo.features[0].geometry.coordinates
            : turf.center(targetGeo.features[0].geometry).geometry.coordinates;
            
          item.userData.geo = {
            type: "LineString",
            coordinates: [start, end]
          };
          
          item.userData.name = `${sourceGeo.features[0].properties.devName}-${targetGeo.features[0].properties.devName}`;
        } else {
          nolineLinkST.push(`${item.source}-${item.target}`);
        }
      }
    }));
  }
  
  return { noFindLinkPsrIds, nolineLinkST };
}

// Retry failed items with exponential backoff
async function retryFailedItems(items, processFn, maxRetries = 3) {
  let failedItems = [...items];
  const allFailedItems = [];
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    if (failedItems.length === 0) break;
    
    // Add delay between retries (exponential backoff)
    if (attempt > 1) {
      await new Promise(resolve => setTimeout(resolve, 500 * Math.pow(2, attempt - 1)));
    }
    
    const currentBatch = [...failedItems];
    failedItems = [];
    
    const results = await processFn(currentBatch);
    
    // Collect all failed items across attempts
    allFailedItems.push(...results.failedItems);
    
    // Update failedItems for next attempt
    failedItems = results.newFailedItems;
  }
  
  return allFailedItems;
}

export async function getGeoGraph(param) {
  const graph = param;
  const { nodes, links } = graph;
  
  // Process nodes in batches with retries
  const { 
    noFindNodeIds, 
    noFindPowerSourceIds, 
    noFindStationIds 
  } = await processNodesInBatches(nodes);
  
  // Process links in batches with retries
  const { 
    noFindLinkPsrIds, 
    nolineLinkST 
  } = await processLinksInBatches(links);
  
  // Store the failed items in the graph
  graph.noFindNodeIds = noFindNodeIds;
  graph.noFindPowerSourceIds = noFindPowerSourceIds;
  graph.noFindStationIds = noFindStationIds;
  graph.noFindLinkPsrIds = noFindLinkPsrIds;
  graph.nolineLinkST = nolineLinkST;
  
  // For very large graphs, consider streaming the output instead of keeping it all in memory
  console.log("Graph processing complete");
  
  // Download the result
  downloadText("resgraph", JSON.stringify(graph));
  
  return graph;
}